/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.wagon.http;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import org.apache.maven.wagon.FileTestUtils;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.StreamingWagon;
import org.apache.maven.wagon.StreamingWagonTestCase;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.Wagon;
import org.apache.maven.wagon.WagonException;
import org.apache.maven.wagon.authentication.AuthenticationInfo;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.proxy.ProxyInfoProvider;
import org.apache.maven.wagon.repository.Repository;
import org.apache.maven.wagon.resource.Resource;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password;

import static org.junit.Assert.assertArrayEquals;

/**
 *
 */
public abstract class HttpWagonTestCase extends StreamingWagonTestCase {
    public static final int SC_TOO_MANY_REQUESTS = 429;

    private Server server;
    private ServerConnector connector;

    protected int getLocalPort(Server server) {
        Connector connector = server.getConnectors()[0];
        return ((ServerConnector) connector).getLocalPort();
    }

    protected void setupWagonTestingFixtures() throws Exception {
        // File round trip testing

        File file = FileTestUtils.createUniqueFile("local-repository", "test-resource");

        file.delete();

        file.getParentFile().mkdirs();

        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory(repositoryDirectory);
        repositoryDirectory.mkdirs();

        server = new Server();
        connector = addConnector(server);

        PutHandler putHandler = new PutHandler(repositoryDirectory);

        ServletContextHandler context = createContext(server, repositoryDirectory);
        HandlerCollection handlers = new HandlerCollection();
        handlers.addHandler(putHandler);
        handlers.addHandler(context);
        server.setHandler(handlers);

        server.start();
    }

    protected final int getTestRepositoryPort() {
        if (server == null) {
            return 0;
        }
        return connector.getLocalPort();
    }

    protected ServletContextHandler createContext(Server server, File repositoryDirectory) throws IOException {
        ServletContextHandler root = new ServletContextHandler(ServletContextHandler.SESSIONS);
        root.setResourceBase(repositoryDirectory.getAbsolutePath());
        ServletHolder servletHolder = new ServletHolder(new DefaultServlet());
        root.addServlet(servletHolder, "/*");
        return root;
    }

    protected void tearDownWagonTestingFixtures() throws Exception {
        server.stop();
    }

    public void testHttpHeaders() throws Exception {
        Properties headers = new Properties();
        headers.setProperty("User-Agent", "Maven-Wagon/1.0");

        StreamingWagon wagon = (StreamingWagon) getWagon();

        setHttpConfiguration(wagon, headers, new Properties());

        Server server = new Server();
        TestHeaderHandler handler = new TestHeaderHandler();
        server.setHandler(handler);
        ServerConnector serverConnector = addConnector(server);
        server.start();

        wagon.connect(new Repository("id", getProtocol() + "://localhost:" + serverConnector.getLocalPort()));

        wagon.getToStream("resource", new ByteArrayOutputStream());

        wagon.disconnect();

        server.stop();

        assertEquals("Maven-Wagon/1.0", handler.headers.get("User-Agent"));
    }

    /**
     * test set of User-Agent as it's done by aether wagon connector with using setHttpHeaders
     */
    public void testHttpHeadersWithCommonMethods() throws Exception {
        Properties properties = new Properties();
        properties.setProperty("User-Agent", "Maven-Wagon/1.0");

        StreamingWagon wagon = (StreamingWagon) getWagon();

        Method setHttpHeaders = wagon.getClass().getMethod("setHttpHeaders", Properties.class);
        setHttpHeaders.invoke(wagon, properties);

        Server server = new Server();
        ServerConnector serverConnector = addConnector(server);
        TestHeaderHandler handler = new TestHeaderHandler();
        server.setHandler(handler);
        addConnector(server);
        server.start();

        wagon.connect(new Repository("id", getProtocol() + "://localhost:" + serverConnector.getLocalPort()));

        wagon.getToStream("resource", new ByteArrayOutputStream());

        wagon.disconnect();

        server.stop();

        assertEquals("Maven-Wagon/1.0", handler.headers.get("User-Agent"));
    }

    public void testUserAgentHeaderIsPresentByDefault() throws Exception {
        StreamingWagon wagon = (StreamingWagon) getWagon();
        Server server = new Server();
        TestHeaderHandler handler = new TestHeaderHandler();
        server.setHandler(handler);
        addConnector(server);
        server.start();
        wagon.connect(new Repository("id", getProtocol() + "://localhost:" + getLocalPort(server)));
        wagon.getToStream("resource", new ByteArrayOutputStream());
        wagon.disconnect();
        server.stop();

        assertNotNull(
                "default User-Agent header of wagon provider should be present", handler.headers.get("User-Agent"));
    }

    public void testUserAgentHeaderIsPresentOnlyOnceIfSetMultipleTimes() throws Exception {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        // 1. set User-Agent header via HttpConfiguration
        Properties headers1 = new Properties();
        headers1.setProperty("User-Agent", "test-user-agent");
        setHttpConfiguration(wagon, headers1, new Properties());

        // 2. redundantly set User-Agent header via setHttpHeaders()
        Properties headers2 = new Properties();
        headers2.setProperty("User-Agent", "test-user-agent");
        Method setHttpHeaders = wagon.getClass().getMethod("setHttpHeaders", Properties.class);
        setHttpHeaders.invoke(wagon, headers2);

        Server server = new Server();
        TestHeaderHandler handler = new TestHeaderHandler();
        server.setHandler(handler);
        addConnector(server);
        server.start();
        wagon.connect(new Repository("id", getProtocol() + "://localhost:" + getLocalPort(server)));
        wagon.getToStream("resource", new ByteArrayOutputStream());
        wagon.disconnect();
        server.stop();

        assertEquals("test-user-agent", handler.headers.get("User-Agent"));
    }

    protected abstract void setHttpConfiguration(StreamingWagon wagon, Properties headers, Properties params);

    protected ServerConnector addConnector(Server server) {
        ServerConnector serverConnector =
                new ServerConnector(server, new HttpConnectionFactory(new HttpConfiguration()));
        server.addConnector(serverConnector);
        return serverConnector;
    }

    protected String getRepositoryUrl(Server server) {
        int localPort = getLocalPort(server);
        return getProtocol() + "://localhost:" + localPort;
    }

    public void testGetForbidden() throws Exception {
        try {
            runTestGet(HttpServletResponse.SC_FORBIDDEN);
            fail();
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testGet404() throws Exception {
        try {
            runTestGet(HttpServletResponse.SC_NOT_FOUND);
            fail();
        } catch (ResourceDoesNotExistException e) {
            assertTrue(true);
        }
    }

    public void testGet500() throws Exception {
        try {
            runTestGet(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            fail();
        } catch (TransferFailedException e) {
            assertTrue(true);
        }
    }

    private void runTestGet(int status) throws Exception {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server server = createStatusServer(status);
        server.start();

        String baseUrl = getRepositoryUrl(server);
        String resourceName = "resource";
        String serverReasonPhrase = HttpStatus.getCode(status).getMessage();

        wagon.connect(new Repository("id", baseUrl));

        try {
            wagon.getToStream("resource", new ByteArrayOutputStream());
            fail();
        } catch (Exception e) {
            verifyWagonExceptionMessage(e, status, baseUrl + "/" + resourceName, serverReasonPhrase);
            throw e;
        } finally {
            wagon.disconnect();

            server.stop();
        }
    }

    public void testResourceExistsForbidden() throws Exception {
        try {
            runTestResourceExists(HttpServletResponse.SC_FORBIDDEN);
            fail();
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testResourceExists404() throws Exception {
        try {
            assertFalse(runTestResourceExists(HttpServletResponse.SC_NOT_FOUND));
        } catch (ResourceDoesNotExistException e) {
            assertTrue(true);
        }
    }

    public void testResourceExists500() throws Exception {
        try {
            runTestResourceExists(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            fail();
        } catch (TransferFailedException e) {
            assertTrue(true);
        }
    }

    public void testResourceExists429() throws Exception {
        try {

            final AtomicBoolean called = new AtomicBoolean();

            AbstractHandler handler = new AbstractHandler() {
                public void handle(
                        String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                        throws IOException, ServletException {
                    if (called.get()) {
                        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                        baseRequest.setHandled(true);
                    } else {
                        called.set(true);
                        response.setStatus(SC_TOO_MANY_REQUESTS);
                        baseRequest.setHandled(true);
                    }
                }
            };

            StreamingWagon wagon = (StreamingWagon) getWagon();
            Server server = new Server();
            server.setHandler(handler);
            addConnector(server);
            server.start();
            wagon.connect(new Repository("id", getRepositoryUrl(server)));

            try {
                wagon.resourceExists("resource");
            } finally {
                wagon.disconnect();

                server.stop();
            }

            fail();
        } catch (TransferFailedException e) {
            assertTrue(true);
        }
    }

    private boolean runTestResourceExists(int status) throws Exception {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server server = createStatusServer(status);
        server.start();

        wagon.connect(new Repository("id", getRepositoryUrl(server)));

        try {
            return wagon.resourceExists("resource");
        } finally {
            wagon.disconnect();

            server.stop();
        }
    }

    protected long getExpectedLastModifiedOnGet(Repository repository, Resource resource) {
        File file = new File(getRepositoryDirectory(), resource.getName());
        return (file.lastModified() / 1000) * 1000;
    }

    protected File getRepositoryDirectory() {
        return getTestFile("target/test-output/http-repository");
    }

    public void testGzipGet() throws Exception {
        Server server = new Server();

        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        ServletContextHandler root = new ServletContextHandler(ServletContextHandler.SESSIONS);
        root.setResourceBase(localRepositoryPath);
        ServletHolder servletHolder = new ServletHolder(new DefaultServlet());
        servletHolder.setInitParameter("gzip", "true");
        root.addServlet(servletHolder, "/*");
        addConnector(server);
        server.setHandler(root);
        server.start();

        try {
            Wagon wagon = getWagon();

            Repository testRepository = new Repository("id", getRepositoryUrl(server));

            File sourceFile = new File(localRepositoryPath + "/gzip");

            sourceFile.deleteOnExit();

            String resName = "gzip-res.txt";
            String sourceContent = writeTestFile(sourceFile, resName, "gzip");

            wagon.connect(testRepository);

            File destFile = FileTestUtils.createUniqueFile(getName(), getName());

            destFile.deleteOnExit();

            wagon.get("gzip/" + resName, destFile);

            wagon.disconnect();

            String destContent = new String(Files.readAllBytes(destFile.toPath()));

            assertEquals(sourceContent, destContent);
        } finally {
            server.stop();
        }
    }

    public void testProxiedRequest() throws Exception {
        ProxyInfo proxyInfo = createProxyInfo();
        TestHeaderHandler handler = new TestHeaderHandler();

        runTestProxiedRequest(proxyInfo, handler);
    }

    public void testProxiedRequestWithAuthentication() throws Exception {
        ProxyInfo proxyInfo = createProxyInfo();
        proxyInfo.setUserName("user");
        proxyInfo.setPassword("secret");
        AuthorizingProxyHandler handler = new AuthorizingProxyHandler();

        runTestProxiedRequest(proxyInfo, handler);

        assertTrue(handler.headers.containsKey("Proxy-Authorization"));

        if (supportProxyPreemptiveAuthentication()) {
            assertEquals(HttpServletResponse.SC_OK, handler.handlerRequestResponses.get(0).responseCode);
        } else {
            assertEquals(
                    HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED,
                    handler.handlerRequestResponses.get(0).responseCode);
            assertEquals(HttpServletResponse.SC_OK, handler.handlerRequestResponses.get(1).responseCode);
        }
    }

    public void testProxiedRequestWithAuthenticationWithProvider() throws Exception {
        final ProxyInfo proxyInfo = createProxyInfo();
        proxyInfo.setUserName("user");
        proxyInfo.setPassword("secret");
        AuthorizingProxyHandler handler = new AuthorizingProxyHandler();

        ProxyInfoProvider proxyInfoProvider = new ProxyInfoProvider() {
            public ProxyInfo getProxyInfo(String protocol) {
                return proxyInfo;
            }
        };
        runTestProxiedRequestWithProvider(proxyInfoProvider, handler);

        assertTrue(handler.headers.containsKey("Proxy-Authorization"));

        if (supportProxyPreemptiveAuthentication()) {
            assertEquals(HttpServletResponse.SC_OK, handler.handlerRequestResponses.get(0).responseCode);
        } else {
            assertEquals(
                    HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED,
                    handler.handlerRequestResponses.get(0).responseCode);
            assertEquals(HttpServletResponse.SC_OK, handler.handlerRequestResponses.get(1).responseCode);
        }
    }

    public void testRedirectGetToStream() throws Exception {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server realServer = new Server();
        TestHeaderHandler handler = new TestHeaderHandler();

        realServer.setHandler(handler);
        addConnector(realServer);
        realServer.start();

        Server redirectServer = new Server();

        addConnector(redirectServer);

        String protocol = getProtocol();

        // protocol is wagon protocol but in fact dav is http(s)
        if (protocol.equals("dav")) {
            protocol = "http";
        }

        if (protocol.equals("davs")) {
            protocol = "https";
        }

        String redirectUrl = protocol + "://localhost:" + getLocalPort(realServer);

        RedirectHandler redirectHandler =
                new RedirectHandler("See Other", HttpServletResponse.SC_SEE_OTHER, redirectUrl, null);

        redirectServer.setHandler(redirectHandler);

        redirectServer.start();

        wagon.connect(new Repository("id", getRepositoryUrl(redirectServer)));

        File tmpResult = File.createTempFile("foo", "get");

        try (FileOutputStream fileOutputStream = new FileOutputStream(tmpResult)) {
            wagon.getToStream("resource", fileOutputStream);
            fileOutputStream.flush();
            fileOutputStream.close();
            String found = new String(Files.readAllBytes(tmpResult.toPath()));
            assertEquals("found:'" + found + "'", "Hello, World!", found);

            checkHandlerResult(redirectHandler.handlerRequestResponses, HttpServletResponse.SC_SEE_OTHER);
            checkHandlerResult(handler.handlerRequestResponses, HttpServletResponse.SC_OK);
        } finally {
            wagon.disconnect();

            redirectServer.stop();
            realServer.stop();

            tmpResult.delete();
        }
    }

    public void testRedirectGet() throws Exception {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server realServer = new Server();
        TestHeaderHandler handler = new TestHeaderHandler();

        realServer.setHandler(handler);
        addConnector(realServer);
        realServer.start();

        Server redirectServer = new Server();

        addConnector(redirectServer);

        String protocol = getProtocol();

        // protocol is wagon protocol but in fact dav is http(s)
        if (protocol.equals("dav")) {
            protocol = "http";
        }

        if (protocol.equals("davs")) {
            protocol = "https";
        }

        String redirectUrl = protocol + "://localhost:" + getLocalPort(realServer);

        RedirectHandler redirectHandler =
                new RedirectHandler("See Other", HttpServletResponse.SC_SEE_OTHER, redirectUrl, null);

        redirectServer.setHandler(redirectHandler);

        redirectServer.start();

        wagon.connect(new Repository("id", getRepositoryUrl(redirectServer)));

        File tmpResult = File.createTempFile("foo", "get");

        try {
            wagon.get("resource", tmpResult);
            String found = new String(Files.readAllBytes(tmpResult.toPath()));
            assertEquals("found:'" + found + "'", "Hello, World!", found);

            checkHandlerResult(redirectHandler.handlerRequestResponses, HttpServletResponse.SC_SEE_OTHER);
            checkHandlerResult(handler.handlerRequestResponses, HttpServletResponse.SC_OK);
        } finally {
            wagon.disconnect();

            redirectServer.stop();
            realServer.stop();

            tmpResult.delete();
        }
    }

    public void testRedirectPutFromStreamWithFullUrl() throws Exception {
        Server realServer = new Server();

        addConnector(realServer);

        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory(repositoryDirectory);
        repositoryDirectory.mkdirs();

        PutHandler putHandler = new PutHandler(repositoryDirectory);

        realServer.setHandler(putHandler);

        realServer.start();

        Server redirectServer = new Server();

        addConnector(redirectServer);

        String protocol = getProtocol();

        // protocol is wagon protocol but in fact dav is http(s)
        if (protocol.equals("dav")) {
            protocol = "http";
        }

        if (protocol.equals("davs")) {
            protocol = "https";
        }

        String redirectUrl = protocol + "://localhost:" + getLocalPort(realServer);

        RedirectHandler redirectHandler =
                new RedirectHandler("See Other", HttpServletResponse.SC_SEE_OTHER, redirectUrl, repositoryDirectory);

        redirectServer.setHandler(redirectHandler);

        redirectServer.start();

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();
            Repository repository = new Repository("foo", getRepositoryUrl(redirectServer));
            wagon.connect(repository);

            File sourceFile = new File(repositoryDirectory, "test-secured-put-resource");
            sourceFile.delete();
            assertFalse(sourceFile.exists());

            File tempFile = File.createTempFile("wagon", "tmp");
            tempFile.deleteOnExit();
            String content = "put top secret";
            Files.write(tempFile.toPath().toAbsolutePath(), content.getBytes(StandardCharsets.UTF_8));

            try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
                wagon.putFromStream(fileInputStream, "test-secured-put-resource", content.length(), -1);
                String actual =
                        new String(Files.readAllBytes(sourceFile.toPath().toAbsolutePath()), StandardCharsets.UTF_8);
                assertEquals(content, actual);

                checkRequestResponseForRedirectPutWithFullUrl(redirectHandler, putHandler);
            } finally {
                wagon.disconnect();
                tempFile.delete();
            }

        } finally {
            realServer.stop();
            redirectServer.stop();
        }
    }

    protected void checkRequestResponseForRedirectPutWithFullUrl(
            RedirectHandler redirectHandler, PutHandler putHandler) {
        checkHandlerResult(redirectHandler.handlerRequestResponses, HttpServletResponse.SC_SEE_OTHER);
        checkHandlerResult(putHandler.handlerRequestResponses, HttpServletResponse.SC_CREATED);
    }

    public void testRedirectPutFromStreamRelativeUrl() throws Exception {
        Server realServer = new Server();
        addConnector(realServer);
        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory(repositoryDirectory);
        repositoryDirectory.mkdirs();

        PutHandler putHandler = new PutHandler(repositoryDirectory);

        realServer.setHandler(putHandler);

        realServer.start();

        Server redirectServer = new Server();

        addConnector(redirectServer);

        RedirectHandler redirectHandler = new RedirectHandler(
                "See Other", HttpServletResponse.SC_SEE_OTHER, "/redirectRequest/foo", repositoryDirectory);

        redirectServer.setHandler(redirectHandler);

        redirectServer.start();

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();
            Repository repository = new Repository("foo", getRepositoryUrl(redirectServer));
            wagon.connect(repository);

            File sourceFile = new File(repositoryDirectory, "/redirectRequest/foo/test-secured-put-resource");
            sourceFile.delete();
            assertFalse(sourceFile.exists());

            File tempFile = File.createTempFile("wagon", "tmp");
            tempFile.deleteOnExit();
            String content = "put top secret";
            Files.write(tempFile.toPath().toAbsolutePath(), content.getBytes(StandardCharsets.UTF_8));

            try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
                wagon.putFromStream(fileInputStream, "test-secured-put-resource", content.length(), -1);
                assertEquals(
                        content,
                        new String(Files.readAllBytes(sourceFile.toPath().toAbsolutePath()), StandardCharsets.UTF_8));

                checkRequestResponseForRedirectPutWithRelativeUrl(redirectHandler, putHandler);
            } finally {
                wagon.disconnect();
                tempFile.delete();
            }

        } finally {
            realServer.stop();
            redirectServer.stop();
        }
    }

    protected void checkRequestResponseForRedirectPutWithRelativeUrl(
            RedirectHandler redirectHandler, PutHandler putHandler) {
        checkHandlerResult(
                redirectHandler.handlerRequestResponses,
                HttpServletResponse.SC_SEE_OTHER,
                HttpServletResponse.SC_CREATED);
        checkHandlerResult(putHandler.handlerRequestResponses);
    }

    protected void checkHandlerResult(
            List<HandlerRequestResponse> handlerRequestResponses, int... expectedResponseCodes) {
        boolean success = true;
        if (handlerRequestResponses.size() == expectedResponseCodes.length) {
            for (int i = 0; i < expectedResponseCodes.length; i++) {
                success &= expectedResponseCodes[i] == handlerRequestResponses.get(i).responseCode;
            }
        }

        if (!success) {
            fail("expected " + expectedResponseCodes + ", got " + handlerRequestResponses);
        }
    }

    public void testRedirectPutFileWithFullUrl() throws Exception {
        Server realServer = new Server();

        addConnector(realServer);

        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory(repositoryDirectory);
        repositoryDirectory.mkdirs();

        PutHandler putHandler = new PutHandler(repositoryDirectory);

        realServer.setHandler(putHandler);

        realServer.start();

        Server redirectServer = new Server();

        addConnector(redirectServer);

        String protocol = getProtocol();

        // protocol is wagon protocol but in fact dav is http(s)
        if (protocol.equals("dav")) {
            protocol = "http";
        }

        if (protocol.equals("davs")) {
            protocol = "https";
        }

        String redirectUrl = protocol + "://localhost:" + getLocalPort(realServer);

        RedirectHandler redirectHandler =
                new RedirectHandler("See Other", HttpServletResponse.SC_SEE_OTHER, redirectUrl, repositoryDirectory);

        redirectServer.setHandler(redirectHandler);

        redirectServer.start();

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();
            Repository repository = new Repository("foo", getRepositoryUrl(redirectServer));
            wagon.connect(repository);

            File sourceFile = new File(repositoryDirectory, "test-secured-put-resource");
            sourceFile.delete();
            assertFalse(sourceFile.exists());

            File tempFile = File.createTempFile("wagon", "tmp");
            tempFile.deleteOnExit();
            byte[] content = "put top secret".getBytes(StandardCharsets.UTF_8);
            Files.write(tempFile.toPath().toAbsolutePath(), content);

            try {
                wagon.put(tempFile, "test-secured-put-resource");
                assertArrayEquals(
                        content, Files.readAllBytes(sourceFile.toPath().toAbsolutePath()));

                checkRequestResponseForRedirectPutWithFullUrl(redirectHandler, putHandler);
            } finally {
                wagon.disconnect();
                tempFile.delete();
            }

        } finally {
            realServer.stop();
            redirectServer.stop();
        }
    }

    public void testRedirectPutFileRelativeUrl() throws Exception {
        Server realServer = new Server();
        addConnector(realServer);
        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory(repositoryDirectory);
        repositoryDirectory.mkdirs();

        PutHandler putHandler = new PutHandler(repositoryDirectory);

        realServer.setHandler(putHandler);

        realServer.start();

        Server redirectServer = new Server();

        addConnector(redirectServer);

        RedirectHandler redirectHandler = new RedirectHandler(
                "See Other", HttpServletResponse.SC_SEE_OTHER, "/redirectRequest/foo", repositoryDirectory);

        redirectServer.setHandler(redirectHandler);

        redirectServer.start();

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();
            Repository repository = new Repository("foo", getRepositoryUrl(redirectServer));
            wagon.connect(repository);

            File sourceFile = new File(repositoryDirectory, "/redirectRequest/foo/test-secured-put-resource");
            sourceFile.delete();
            assertFalse(sourceFile.exists());

            File tempFile = File.createTempFile("wagon", "tmp");
            tempFile.deleteOnExit();
            byte[] content = "put top secret".getBytes(StandardCharsets.UTF_8);
            Files.write(tempFile.toPath().toAbsolutePath(), content);

            try {
                wagon.put(tempFile, "test-secured-put-resource");
                assertArrayEquals(
                        content, Files.readAllBytes(sourceFile.toPath().toAbsolutePath()));

                checkRequestResponseForRedirectPutWithRelativeUrl(redirectHandler, putHandler);
            } finally {
                wagon.disconnect();
                tempFile.delete();
            }

        } finally {
            realServer.stop();
            redirectServer.stop();
        }
    }

    public void testRedirectPutFailureNonRepeatableStream() throws Exception {
        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory(repositoryDirectory);
        repositoryDirectory.mkdirs();

        Server redirectServer = new Server();

        addConnector(redirectServer);

        RedirectHandler redirectHandler =
                new RedirectHandler("See Other", HttpServletResponse.SC_SEE_OTHER, "/redirectRequest/foo", null);

        redirectServer.setHandler(redirectHandler);

        redirectServer.start();

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Properties params = new Properties();
            params.put("http.protocol.expect-continue", "%b,false");
            setHttpConfiguration(wagon, new Properties(), params);
            Repository repository = new Repository("foo", getRepositoryUrl(redirectServer));
            wagon.connect(repository);

            File sourceFile = new File(repositoryDirectory, "/redirectRequest/foo/test-secured-put-resource");
            sourceFile.delete();
            assertFalse(sourceFile.exists());

            File tempFile = File.createTempFile("wagon", "tmp");
            tempFile.deleteOnExit();
            String content = "put top secret";
            Files.write(tempFile.toPath().toAbsolutePath(), content.getBytes(StandardCharsets.UTF_8));

            try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
                wagon.putFromStream(fileInputStream, "test-secured-put-resource", content.length(), -1);
                // This does not behave as expected because LightweightWagon does buffering by default
                if (wagon.getClass().getName().contains("Lightweight")) {
                    assertTrue(true);
                } else {
                    fail();
                }
            } catch (TransferFailedException e) {
                assertTrue(true);
            } finally {
                wagon.disconnect();
                tempFile.delete();
            }

        } finally {
            redirectServer.stop();
        }
    }

    /**
     *
     */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    public static class RedirectHandler extends AbstractHandler {
        String reason;

        int retCode;

        String redirectUrl;

        File repositoryDirectory;

        public List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<>();

        RedirectHandler(String reason, int retCode, String redirectUrl, File repositoryDirectory) {
            this.reason = reason;
            this.retCode = retCode;
            this.redirectUrl = redirectUrl;
            this.repositoryDirectory = repositoryDirectory;
        }

        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException {
            if (request.getRequestURI().contains("redirectRequest")) {
                PutHandler putHandler = new PutHandler(this.repositoryDirectory);
                putHandler.handle(target, baseRequest, request, response);
                handlerRequestResponses.add(
                        new HandlerRequestResponse(request.getMethod(), response.getStatus(), request.getRequestURI()));
                return;
            }
            response.setStatus(this.retCode);
            response.setHeader("Location", this.redirectUrl + request.getRequestURI());
            baseRequest.setHandled(true);

            handlerRequestResponses.add(
                    new HandlerRequestResponse(request.getMethod(), response.getStatus(), request.getRequestURI()));
        }
    }

    private void runTestProxiedRequest(ProxyInfo proxyInfo, TestHeaderHandler handler) throws Exception {
        // what an UGLY hack!
        // but apparently jetty needs some time to free up resources
        // <5s: broken test :(
        // CHECKSTYLE_OFF: MagicNumber
        Thread.sleep(5001L);
        // CHECKSTYLE_ON: MagicNumber

        Server proxyServer = new Server();
        ServerConnector serverConnector =
                new ServerConnector(proxyServer, new HttpConnectionFactory(new HttpConfiguration()));
        proxyServer.addConnector(serverConnector);
        proxyServer.setHandler(handler);

        proxyServer.start();

        proxyInfo.setPort(getLocalPort(proxyServer));

        System.out.println("start proxy on host/port " + proxyInfo.getHost() + "/" + proxyInfo.getPort()
                + " with non proxyHosts " + proxyInfo.getNonProxyHosts());

        while (!proxyServer.isRunning() || !proxyServer.isStarted()) {
            Thread.sleep(10);
        }

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository("id", "http://www.example.com/");

            String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
            File sourceFile = new File(localRepositoryPath, "test-proxied-resource");
            Files.write(sourceFile.toPath().toAbsolutePath(), "content".getBytes(StandardCharsets.UTF_8));

            wagon.connect(testRepository, proxyInfo);

            try {
                wagon.getToStream("test-proxied-resource", new ByteArrayOutputStream());

                assertTrue(handler.headers.containsKey("Proxy-Connection"));
            } finally {
                System.setProperty("http.proxyHost", "");
                System.setProperty("http.proxyPort", "");
                wagon.disconnect();
            }
        } finally {
            proxyServer.stop();
        }
    }

    private void runTestProxiedRequestWithProvider(ProxyInfoProvider proxyInfoProvider, TestHeaderHandler handler)
            throws Exception {
        // what an UGLY hack!
        // but apparently jetty needs some time to free up resources
        // <5s: broken test :(
        // CHECKSTYLE_OFF: MagicNumber
        Thread.sleep(5001L);
        // CHECKSTYLE_ON: MagicNumber

        Server proxyServer = new Server();
        ServerConnector serverConnector =
                new ServerConnector(proxyServer, new HttpConnectionFactory(new HttpConfiguration()));
        proxyServer.addConnector(serverConnector);

        proxyServer.setHandler(handler);

        proxyServer.start();

        proxyInfoProvider.getProxyInfo(null).setPort(getLocalPort(proxyServer));

        System.out.println("start proxy on host/port "
                + proxyInfoProvider.getProxyInfo(null).getHost() + "/"
                + proxyInfoProvider.getProxyInfo(null).getPort() + " with non proxyHosts "
                + proxyInfoProvider.getProxyInfo(null).getNonProxyHosts());

        while (!proxyServer.isRunning() || !proxyServer.isStarted()) {
            Thread.sleep(10);
        }

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository("id", "http://www.example.com/");

            String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
            File sourceFile = new File(localRepositoryPath, "test-proxied-resource");
            Files.write(sourceFile.toPath().toAbsolutePath(), "content".getBytes(StandardCharsets.UTF_8));

            wagon.connect(testRepository, proxyInfoProvider);

            try {
                wagon.getToStream("test-proxied-resource", new ByteArrayOutputStream());

                assertTrue(handler.headers.containsKey("Proxy-Connection"));
            } finally {
                System.setProperty("http.proxyHost", "");
                System.setProperty("http.proxyPort", "");
                wagon.disconnect();
            }
        } finally {
            proxyServer.stop();
        }
    }

    private ProxyInfo createProxyInfo() {
        ProxyInfo proxyInfo = new ProxyInfo();
        proxyInfo.setHost("localhost");
        proxyInfo.setNonProxyHosts(null);
        proxyInfo.setType("http");
        return proxyInfo;
    }

    public void testSecuredGetUnauthorized() throws Exception {
        try {
            runTestSecuredGet(null);
            fail();
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testSecuredGetWrongPassword() throws Exception {
        try {
            AuthenticationInfo authInfo = new AuthenticationInfo();
            authInfo.setUserName("user");
            authInfo.setPassword("admin");
            runTestSecuredGet(authInfo);
            fail();
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testSecuredGet() throws Exception {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName("user");
        authInfo.setPassword("secret");
        runTestSecuredGet(authInfo);
    }

    public void runTestSecuredGet(AuthenticationInfo authInfo) throws Exception {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = createSecurityServer(localRepositoryPath);

        server.start();

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository("id", getRepositoryUrl(server));

            File sourceFile = new File(localRepositoryPath, "test-secured-resource");
            Files.write(sourceFile.toPath().toAbsolutePath(), "top secret".getBytes(StandardCharsets.UTF_8));

            wagon.connect(testRepository, authInfo);

            File file = File.createTempFile("wagon-test", "txt");

            try {
                wagon.get("test-secured-resource", file);
            } finally {
                wagon.disconnect();
            }

            assertEquals("top secret", new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8));

            /*
             * We need to wait a bit for all Jetty workers/threads to complete their work. Otherwise,
             * we may suffer from race conditions where handlerRequestResponses list is not completely
             * populated and its premature iteration in testPreemptiveAuthenticationGet will lead to
             * a test failure.
             */
            // CHECKSTYLE_OFF: MagicNumber
            Thread.sleep(2000L);
            // CHECKSTYLE_ON: MagicNumber

            TestSecurityHandler securityHandler = server.getChildHandlerByClass(TestSecurityHandler.class);
            testPreemptiveAuthenticationGet(securityHandler, supportPreemptiveAuthenticationGet());

        } finally {
            server.stop();
        }
    }

    public void testSecuredGetToStream() throws Exception {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName("user");
        authInfo.setPassword("secret");
        runTestSecuredGetToStream(authInfo);
    }

    public void runTestSecuredGetToStream(AuthenticationInfo authInfo) throws Exception {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = createSecurityServer(localRepositoryPath);

        server.start();

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository("id", getRepositoryUrl(server));

            File sourceFile = new File(localRepositoryPath, "test-secured-resource");
            Files.write(sourceFile.toPath().toAbsolutePath(), "top secret".getBytes(StandardCharsets.US_ASCII));

            wagon.connect(testRepository, authInfo);

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            try {
                wagon.getToStream("test-secured-resource", out);
            } finally {
                wagon.disconnect();
            }

            assertEquals("top secret", out.toString("US-ASCII"));

            /*
             * We need to wait a bit for all Jetty workers/threads to complete their work. Otherwise,
             * we may suffer from race conditions where handlerRequestResponses list is not completely
             * populated and its premature iteration in testPreemptiveAuthenticationGet will lead to
             * a test failure.
             */
            // CHECKSTYLE_OFF: MagicNumber
            Thread.sleep(2000L);
            // CHECKSTYLE_ON: MagicNumber

            TestSecurityHandler securityHandler = server.getChildHandlerByClass(TestSecurityHandler.class);
            testPreemptiveAuthenticationGet(securityHandler, supportPreemptiveAuthenticationGet());
        } finally {
            server.stop();
        }
    }

    public void testSecuredResourceExistsUnauthorized() throws Exception {
        try {
            runTestSecuredResourceExists(null);
            fail();
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testSecuredResourceExistsWrongPassword() throws Exception {
        try {
            AuthenticationInfo authInfo = new AuthenticationInfo();
            authInfo.setUserName("user");
            authInfo.setPassword("admin");
            runTestSecuredResourceExists(authInfo);
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testSecuredResourceExists() throws Exception {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName("user");
        authInfo.setPassword("secret");
        runTestSecuredResourceExists(authInfo);
    }

    public void runTestSecuredResourceExists(AuthenticationInfo authInfo) throws Exception {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = createSecurityServer(localRepositoryPath);

        server.start();

        try {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository("id", getRepositoryUrl(server));

            File sourceFile = new File(localRepositoryPath, "test-secured-resource-exists");
            Files.write(sourceFile.toPath().toAbsolutePath(), "top secret".getBytes(StandardCharsets.US_ASCII));

            wagon.connect(testRepository, authInfo);

            try {
                assertTrue(wagon.resourceExists("test-secured-resource-exists"));

                assertFalse(wagon.resourceExists("test-secured-resource-not-exists"));
            } finally {
                wagon.disconnect();
            }
        } finally {
            server.stop();
        }
    }

    private Server createSecurityServer(String localRepositoryPath) {
        Server server = new Server();

        SecurityHandler sh = createSecurityHandler();

        ServletContextHandler root =
                new ServletContextHandler(ServletContextHandler.SESSIONS | ServletContextHandler.SECURITY);
        root.setResourceBase(localRepositoryPath);
        root.setSecurityHandler(sh);
        ServletHolder servletHolder = new ServletHolder(new DefaultServlet());
        root.addServlet(servletHolder, "/*");

        server.setHandler(root);
        addConnector(server);
        return server;
    }

    private Server createStatusServer(int status) {
        Server server = new Server();
        StatusHandler handler = new StatusHandler();
        handler.setStatusToReturn(status);
        server.setHandler(handler);
        addConnector(server);
        return server;
    }

    private String writeTestFile(File parent, String child, String compressionType) throws IOException {
        File file = new File(parent, child);
        file.getParentFile().mkdirs();
        file.deleteOnExit();
        OutputStream out = new FileOutputStream(file);
        try {
            out.write(child.getBytes());
        } finally {
            out.close();
        }

        String ext = "";
        if ("gzip".equals(compressionType)) {
            ext = ".gz";
        }
        if ("deflate".equals(compressionType)) {
            ext = ".deflate";
        }

        file = new File(parent, child + ext);
        file.deleteOnExit();
        String content;
        out = new FileOutputStream(file);
        if ("gzip".equals(compressionType)) {
            out = new GZIPOutputStream(out);
        }
        if ("deflate".equals(compressionType)) {
            out = new DeflaterOutputStream(out);
        }
        try {
            // write out different data than non-compressed file, so we can
            // assert the compressed version was returned
            content = file.getAbsolutePath();
            out.write(content.getBytes());
        } finally {
            out.close();
        }

        return content;
    }

    public void testPutForbidden() throws Exception {
        try {
            runTestPut(HttpServletResponse.SC_FORBIDDEN);
            fail();
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testPut404() throws Exception {
        try {
            runTestPut(HttpServletResponse.SC_NOT_FOUND);
            fail();
        } catch (ResourceDoesNotExistException e) {
            assertTrue(true);
        }
    }

    public void testPut500() throws Exception {
        try {
            runTestPut(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            fail();
        } catch (TransferFailedException e) {
            assertTrue(true);
        }
    }

    public void testPut429() throws Exception {

        try {

            StreamingWagon wagon = (StreamingWagon) getWagon();
            Server server = new Server();
            final AtomicBoolean called = new AtomicBoolean();

            AbstractHandler handler = new AbstractHandler() {
                public void handle(
                        String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                        throws IOException, ServletException {
                    if (called.get()) {
                        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                        baseRequest.setHandled(true);
                    } else {
                        called.set(true);
                        response.setStatus(SC_TOO_MANY_REQUESTS);
                        baseRequest.setHandled(true);
                    }
                }
            };

            server.setHandler(handler);
            addConnector(server);
            server.start();

            wagon.connect(new Repository("id", getRepositoryUrl(server)));

            File tempFile = File.createTempFile("wagon", "tmp");
            tempFile.deleteOnExit();
            Files.write(tempFile.toPath().toAbsolutePath(), "content".getBytes(StandardCharsets.US_ASCII));

            try {
                wagon.put(tempFile, "resource");
                fail();
            } finally {
                wagon.disconnect();

                server.stop();

                tempFile.delete();
            }

        } catch (TransferFailedException e) {
            assertTrue(true);
        }
    }

    private void runTestPut(int status) throws Exception {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server server = createStatusServer(status);
        server.start();

        wagon.connect(new Repository("id", getRepositoryUrl(server)));

        File tempFile = File.createTempFile("wagon", "tmp");
        tempFile.deleteOnExit();
        Files.write(tempFile.toPath().toAbsolutePath(), "content".getBytes(StandardCharsets.US_ASCII));

        String baseUrl = getRepositoryUrl(server);
        String resourceName = "resource";
        String serverReasonPhrase = HttpStatus.getCode(status).getMessage();

        try {
            wagon.put(tempFile, resourceName);
            fail();
        } catch (Exception e) {
            verifyWagonExceptionMessage(e, status, baseUrl + "/" + resourceName, serverReasonPhrase);
            throw e;
        } finally {
            wagon.disconnect();

            server.stop();

            tempFile.delete();
        }
    }

    public void testSecuredPutUnauthorized() throws Exception {
        try {
            runTestSecuredPut(null);
            fail();
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testSecuredPutWrongPassword() throws Exception {
        try {
            AuthenticationInfo authInfo = new AuthenticationInfo();
            authInfo.setUserName("user");
            authInfo.setPassword("admin");
            runTestSecuredPut(authInfo);
            fail();
        } catch (AuthorizationException e) {
            assertTrue(true);
        }
    }

    public void testSecuredPut() throws Exception {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName("user");
        authInfo.setPassword("secret");
        runTestSecuredPut(authInfo);
    }

    public void runTestSecuredPut(AuthenticationInfo authInfo) throws Exception {
        runTestSecuredPut(authInfo, 1);
    }

    public void runTestSecuredPut(AuthenticationInfo authInfo, int putNumber) throws Exception {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = new Server();

        TestSecurityHandler sh = createSecurityHandler();

        PutHandler putHandler = new PutHandler(new File(localRepositoryPath));

        sh.setHandler(putHandler);
        server.setHandler(sh);
        addConnector(server);
        server.start();

        StreamingWagon wagon = (StreamingWagon) getWagon();
        Repository testRepository = new Repository("id", getRepositoryUrl(server));
        wagon.connect(testRepository, authInfo);
        try {
            for (int i = 0; i < putNumber; i++) {
                File sourceFile = new File(localRepositoryPath, "test-secured-put-resource");
                sourceFile.delete();
                assertFalse(sourceFile.exists());

                File tempFile = File.createTempFile("wagon", "tmp");
                tempFile.deleteOnExit();
                byte[] content = "put top secret".getBytes(StandardCharsets.US_ASCII);
                Files.write(tempFile.toPath().toAbsolutePath(), content);

                try {
                    wagon.put(tempFile, "test-secured-put-resource");
                } finally {
                    tempFile.delete();
                }

                assertArrayEquals(
                        content, Files.readAllBytes(sourceFile.toPath().toAbsolutePath()));
            }
        } finally {
            wagon.disconnect();
            server.stop();
        }
        assertEquals(putNumber, putHandler.putCallNumber);
        testPreemptiveAuthenticationPut(sh, supportPreemptiveAuthenticationPut());
    }

    public void testNonSecuredPutFromStream() throws Exception {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName("user");
        authInfo.setPassword("secret");
        runTestSecuredPutFromStream(authInfo, 1, false);
    }

    public void testSecuredPutFromStream() throws Exception {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName("user");
        authInfo.setPassword("secret");
        runTestSecuredPutFromStream(authInfo, 1, true);
    }

    public void runTestSecuredPutFromStream(AuthenticationInfo authInfo, int putNumber, boolean addSecurityHandler)
            throws Exception {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = new Server();

        TestSecurityHandler sh = createSecurityHandler();

        PutHandler putHandler = new PutHandler(new File(localRepositoryPath));

        if (addSecurityHandler) {
            sh.setHandler(putHandler);
            server.setHandler(sh);
        } else {
            server.setHandler(putHandler);
        }
        addConnector(server);
        server.start();

        StreamingWagon wagon = (StreamingWagon) getWagon();
        Repository testRepository = new Repository("id", getRepositoryUrl(server));
        if (addSecurityHandler) {
            wagon.connect(testRepository, authInfo);
        } else {
            wagon.connect(testRepository);
        }
        try {
            for (int i = 0; i < putNumber; i++) {
                File sourceFile = new File(localRepositoryPath, "test-secured-put-resource");
                sourceFile.delete();
                assertFalse(sourceFile.exists());

                File tempFile = File.createTempFile("wagon", "tmp");
                tempFile.deleteOnExit();
                byte[] content = "put top secret".getBytes(StandardCharsets.UTF_8);
                Files.write(tempFile.toPath().toAbsolutePath(), content);

                try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
                    wagon.putFromStream(fileInputStream, "test-secured-put-resource", content.length, -1);
                } finally {
                    tempFile.delete();
                }

                assertArrayEquals(
                        content, Files.readAllBytes(sourceFile.toPath().toAbsolutePath()));
            }
        } finally {
            wagon.disconnect();
            server.stop();
        }
        assertEquals(putNumber, putHandler.putCallNumber);
        if (addSecurityHandler) {
            testPreemptiveAuthenticationPut(sh, supportPreemptiveAuthenticationPut());
        }

        // ensure we didn't use chunked transfer which doesn't work on ngnix
        for (DeployedResource deployedResource : putHandler.deployedResources) {
            if (StringUtils.equalsIgnoreCase("chunked", deployedResource.transferEncoding)) {
                fail("deployedResource use chunked: " + deployedResource);
            }
        }
    }

    protected abstract boolean supportPreemptiveAuthenticationPut();

    protected abstract boolean supportPreemptiveAuthenticationGet();

    protected abstract boolean supportProxyPreemptiveAuthentication();

    protected void testPreemptiveAuthenticationGet(TestSecurityHandler sh, boolean preemptive) {
        testPreemptiveAuthentication(sh, preemptive, HttpServletResponse.SC_OK);
    }

    protected void testPreemptiveAuthenticationPut(TestSecurityHandler sh, boolean preemptive) {
        testPreemptiveAuthentication(sh, preemptive, HttpServletResponse.SC_CREATED);
    }

    protected void testPreemptiveAuthentication(TestSecurityHandler sh, boolean preemptive, int statusCode) {

        if (preemptive) {
            assertEquals(
                    "not 1 security handler use " + sh.handlerRequestResponses, 1, sh.handlerRequestResponses.size());
            assertEquals(statusCode, sh.handlerRequestResponses.get(0).responseCode);
        } else {
            assertEquals(
                    "not 2 security handler use " + sh.handlerRequestResponses, 2, sh.handlerRequestResponses.size());
            assertEquals(HttpServletResponse.SC_UNAUTHORIZED, sh.handlerRequestResponses.get(0).responseCode);
            assertEquals(statusCode, sh.handlerRequestResponses.get(1).responseCode);
        }
    }

    static class StatusHandler extends AbstractHandler {
        private int status;

        public void setStatusToReturn(int status) {
            this.status = status;
        }

        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException {
            if (status != 0) {
                response.setStatus(status);
                baseRequest.setHandled(true);
            }
        }
    }

    static class DeployedResource {
        String httpMethod;

        String requestUri;

        String contentLength;

        String transferEncoding;

        DeployedResource() {
            // no op
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("DeployedResource");
            sb.append("{httpMethod='").append(httpMethod).append('\'');
            sb.append(", requestUri='").append(requestUri).append('\'');
            sb.append(", contentLength='").append(contentLength).append('\'');
            sb.append(", transferEncoding='").append(transferEncoding).append('\'');
            sb.append('}');
            return sb.toString();
        }
    }

    /**
     *
     */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    public static class PutHandler extends AbstractHandler {
        private final File resourceBase;

        public List<DeployedResource> deployedResources = new ArrayList<>();

        public int putCallNumber = 0;

        public List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<>();

        public PutHandler(File repositoryDirectory) {
            this.resourceBase = repositoryDirectory;
        }

        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException {
            if (baseRequest.isHandled() || !"PUT".equals(baseRequest.getMethod())) {
                return;
            }

            baseRequest.setHandled(true);

            File file = new File(resourceBase, URLDecoder.decode(request.getPathInfo()));
            file.getParentFile().mkdirs();
            OutputStream out = null;
            InputStream in = null;
            try {
                in = request.getInputStream();
                out = new FileOutputStream(file);
                IOUtil.copy(in, out);
                out.close();
                out = null;
                in.close();
                in = null;
            } finally {
                IOUtil.close(in);
                IOUtil.close(out);
            }
            putCallNumber++;
            DeployedResource deployedResource = new DeployedResource();

            deployedResource.httpMethod = request.getMethod();
            deployedResource.requestUri = request.getRequestURI();
            deployedResource.transferEncoding = request.getHeader("Transfer-Encoding");
            deployedResource.contentLength = request.getHeader("Content-Length");
            deployedResources.add(deployedResource);

            response.setStatus(HttpServletResponse.SC_CREATED);

            handlerRequestResponses.add(new HandlerRequestResponse(
                    request.getMethod(), ((Response) response).getStatus(), request.getRequestURI()));
        }
    }

    private static class AuthorizingProxyHandler extends TestHeaderHandler {

        List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<>();

        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException {
            System.out.println(" handle proxy request");
            if (request.getHeader("Proxy-Authorization") == null) {
                handlerRequestResponses.add(new HandlerRequestResponse(
                        request.getMethod(),
                        HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED,
                        request.getRequestURI()));
                response.setStatus(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED);
                response.addHeader("Proxy-Authenticate", "Basic realm=\"Squid proxy-caching web server\"");

                baseRequest.setHandled(true);
                return;
            }
            handlerRequestResponses.add(new HandlerRequestResponse(
                    request.getMethod(), HttpServletResponse.SC_OK, request.getRequestURI()));
            super.handle(target, baseRequest, request, response);
        }
    }

    /**
     *
     */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    private static class TestHeaderHandler extends AbstractHandler {
        public Map<String, String> headers = Collections.emptyMap();

        public List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<>();

        TestHeaderHandler() {}

        public void handle(
                String target, Request baseRrequest, HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException {
            headers = new HashMap<>();
            for (Enumeration<String> e = baseRrequest.getHeaderNames(); e.hasMoreElements(); ) {
                String name = e.nextElement();
                Enumeration headerValues = baseRrequest.getHeaders(name);
                // as per HTTP spec http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
                // multiple values for the same header key are concatenated separated by comma
                // otherwise we wouldn't notice headers with same key added multiple times
                StringBuffer combinedHeaderValue = new StringBuffer();
                for (int i = 0; headerValues.hasMoreElements(); i++) {
                    if (i > 0) {
                        combinedHeaderValue.append(",");
                    }
                    combinedHeaderValue.append(headerValues.nextElement());
                }
                headers.put(name, combinedHeaderValue.toString());
            }

            response.setContentType("text/plain");
            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().print("Hello, World!");

            handlerRequestResponses.add(new HandlerRequestResponse(
                    baseRrequest.getMethod(), ((Response) response).getStatus(), baseRrequest.getRequestURI()));

            baseRrequest.setHandled(true);
        }
    }

    protected TestSecurityHandler createSecurityHandler() {
        Constraint constraint = new Constraint();
        constraint.setName(Constraint.__BASIC_AUTH);
        constraint.setRoles(new String[] {"admin"});
        constraint.setAuthenticate(true);

        ConstraintMapping cm = new ConstraintMapping();
        cm.setConstraint(constraint);
        cm.setPathSpec("/*");

        TestSecurityHandler sh = new TestSecurityHandler();
        HashLoginService hashLoginService = new HashLoginService("MyRealm");
        hashLoginService.putUser("user", new Password("secret"), new String[] {"admin"});
        sh.setLoginService(hashLoginService);
        sh.setConstraintMappings(new ConstraintMapping[] {cm});
        sh.setAuthenticator(new BasicAuthenticator());
        return sh;
    }

    /**
     *
     */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    public static class TestSecurityHandler extends ConstraintSecurityHandler {

        public List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<>();

        @Override
        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException {
            String method = request.getMethod();
            super.handle(target, baseRequest, request, response);

            handlerRequestResponses.add(
                    new HandlerRequestResponse(method, ((Response) response).getStatus(), request.getRequestURI()));
        }
    }

    /**
     *
     */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    public static class HandlerRequestResponse {
        public String method;

        public int responseCode;

        public String requestUri;

        private HandlerRequestResponse(String method, int responseCode, String requestUri) {
            this.method = method;
            this.responseCode = responseCode;
            this.requestUri = requestUri;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("HandlerRequestResponse");
            sb.append("{method='").append(method).append('\'');
            sb.append(", responseCode=").append(responseCode);
            sb.append(", requestUri='").append(requestUri).append('\'');
            sb.append('}');
            return sb.toString();
        }
    }

    /**
     * Verify a WagonException message contains required format and context based on the status code we expected to
     * trigger it in the first place.
     * <p>
     * This implementation represents the most desired assertions, but HttpWagonTestCase sub-classes could override
     * this method if a specific wagon representation makes it impossible to meet these assertions.
     *
     * @param e               an instance of {@link WagonException}
     * @param forStatusCode   the response status code that triggered the exception
     * @param forUrl          the url that triggered the exception
     * @param forReasonPhrase the optional status line reason phrase the server returned
     */
    protected void verifyWagonExceptionMessage(Exception e, int forStatusCode, String forUrl, String forReasonPhrase) {
        // TODO: handle AuthenticationException for Wagon.connect() calls
        assertNotNull(e);
        try {
            assertTrue("only verify instances of WagonException", e instanceof WagonException);

            String reasonPhrase;
            String assertMessageForBadMessage = "exception message not described properly";
            switch (forStatusCode) {
                case HttpServletResponse.SC_NOT_FOUND:
                    // TODO: add test for 410: Gone?
                    assertTrue(
                            "404 not found response should throw ResourceDoesNotExistException",
                            e instanceof ResourceDoesNotExistException);
                    reasonPhrase = (forReasonPhrase == null || forReasonPhrase.isEmpty())
                            ? " Not Found"
                            : (" " + forReasonPhrase);
                    assertEquals(
                            assertMessageForBadMessage,
                            "resource missing at " + forUrl + ", status: 404" + reasonPhrase,
                            e.getMessage());
                    break;

                case HttpServletResponse.SC_UNAUTHORIZED:
                    // FIXME assumes Wagon.get()/put() returning 401 instead of Wagon.connect()
                    assertTrue(
                            "401 Unauthorized should throw AuthorizationException since "
                                    + " AuthenticationException is not explicitly declared as thrown from wagon "
                                    + "methods",
                            e instanceof AuthorizationException);
                    reasonPhrase = (forReasonPhrase == null || forReasonPhrase.isEmpty())
                            ? " Unauthorized"
                            : (" " + forReasonPhrase);
                    assertEquals(
                            assertMessageForBadMessage,
                            "authentication failed for " + forUrl + ", status: 401" + reasonPhrase,
                            e.getMessage());
                    break;

                case HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED:
                    assertTrue(
                            "407 Proxy authentication required should throw AuthorizationException",
                            e instanceof AuthorizationException);
                    reasonPhrase = (forReasonPhrase == null || forReasonPhrase.isEmpty())
                            ? " Proxy Authentication Required"
                            : (" " + forReasonPhrase);
                    assertEquals(
                            assertMessageForBadMessage,
                            "proxy authentication failed for " + forUrl + ", status: 407" + reasonPhrase,
                            e.getMessage());
                    break;

                case HttpServletResponse.SC_FORBIDDEN:
                    assertTrue(
                            "403 Forbidden should throw AuthorizationException", e instanceof AuthorizationException);
                    reasonPhrase = (forReasonPhrase == null || forReasonPhrase.isEmpty())
                            ? " Forbidden"
                            : (" " + forReasonPhrase);
                    assertEquals(
                            assertMessageForBadMessage,
                            "authorization failed for " + forUrl + ", status: 403" + reasonPhrase,
                            e.getMessage());
                    break;

                default:
                    assertTrue(
                            "transfer failures should at least be wrapped in a TransferFailedException",
                            e instanceof TransferFailedException);
                    assertTrue(
                            "expected status code for transfer failures should be >= 400",
                            forStatusCode >= HttpServletResponse.SC_BAD_REQUEST);
                    reasonPhrase = forReasonPhrase == null ? "" : " " + forReasonPhrase;
                    assertEquals(
                            assertMessageForBadMessage,
                            "transfer failed for " + forUrl + ", status: " + forStatusCode + reasonPhrase,
                            e.getMessage());
                    break;
            }
        } catch (AssertionError assertionError) {
            logger.error("Exception which failed assertions: ", e);
            throw assertionError;
        }
    }
}
